﻿using System.Text;
using System.Text.RegularExpressions;

namespace DimensionsHelper.LanguageService.Protocol;

public struct DocumentUriComponents
{
    public string? Scheme { get; set; }
    public string? Authority { get; set; }
    public string? Path { get; set; }
    public string? Query { get; set; }
    public string? Fragment { get; set; }
}


/// <summary>
///     Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986.
///     This class is a simple parser which creates the basic component parts
///     (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation
///     and encoding.
///     <code>
///   foo://example.com:8042/over/there?name=ferret#nose
///   \_/   \______________/\_________/ \_________/ \__/
///    |           |            |            |        |
///  scheme     authority      path        query   fragment
///    |   _____________________|__
///   / \ /                        \
///   urn:example:animal:ferret:nose
/// </code>
/// </summary>
public partial class DocumentUri : IEquatable<DocumentUri>
{
    private static readonly Dictionary<char, string> EscapeMap = new()
    {
        [' '] = "%20",
        ['!'] = "%21",
        ['"'] = "%22",
        ['#'] = "%23",
        ['%'] = "%25",
        ['&'] = "%26",
        ['('] = "%28",
        [')'] = "%29",
        ['*'] = "%2A",
        ['+'] = "%2B",
        [','] = "%2C",
        ['/'] = "%2F",
        [':'] = "%3A",
        [';'] = "%3B",
        ['<'] = "%3C",
        ['='] = "%3D",
        ['>'] = "%3E",
        ['?'] = "%3F",
        ['@'] = "%40",
        ['['] = "%5B",
        ['\\'] = "%5C",
        [']'] = "%5D",
        ['|'] = "%7C"
    };


    public DocumentUri(string? scheme, string? authority, string? path, string? query, string? fragment,
        bool? strict = null)
    {
        Scheme = FixSchema(scheme, strict);
        Path = ReferenceResolution(Scheme, path ?? string.Empty);
        Authority = authority ?? string.Empty;
        Query = query ?? string.Empty;
        Fragment = fragment ?? string.Empty;
    }

    /// <summary>
    ///     scheme is the "http' part of 'http://www.msft.com/some/path?query#fragment".
    ///     The part before the first colon.
    /// </summary>
    public string? Scheme { get; }

    /// <summary>
    ///     authority is the "www.msft.com' part of 'http://www.msft.com/some/path?query#fragment".
    ///     The part between the first double slashes and the next slash.
    /// </summary>
    public string Authority { get; }

    /// <summary>
    ///     path is the "/some/path' part of 'http://www.msft.com/some/path?query#fragment".
    /// </summary>
    public string Path { get; }

    /// <summary>
    ///     query is the "query' part of 'http://www.msft.com/some/path?query#fragment".
    /// </summary>
    public string Query { get; }

    /// <summary>
    ///     fragment is the "fragment' part of 'http://www.msft.com/some/path?query#fragment".
    /// </summary>
    public string Fragment { get; }


    public static DocumentUri Empty { get; } =
        new(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty);

    public bool Equals(DocumentUri? other)
    {
        return other != null
               && string.Equals(other.Scheme, Scheme, StringComparison.OrdinalIgnoreCase)
               && string.Equals(other.Fragment, Fragment, StringComparison.OrdinalIgnoreCase)
               && string.Equals(other.Authority, Authority, StringComparison.OrdinalIgnoreCase)
               && string.Equals(other.Query, Query, StringComparison.OrdinalIgnoreCase)
               && string.Equals(other.Path, Path, StringComparison.OrdinalIgnoreCase);
    }


    private static string? FixSchema(string? schema, bool? strict)
    {
        if (string.IsNullOrWhiteSpace(schema) && strict != true)
        {
            return Uri.UriSchemeFile;
        }

        return schema;
    }


    private static string ReferenceResolution(string? schema, string path)
    {
        // the slash-character is our "default base' as we don"t
        // support constructing URIs relative to other URIs. This
        // also means that we alter and potentially break paths.
        // see https://tools.ietf.org/html/rfc3986#section-5.1.4
        if (schema != Uri.UriSchemeHttps && schema != Uri.UriSchemeHttp && schema != Uri.UriSchemeFile)
        {
            return path;
        }

        if (string.IsNullOrWhiteSpace(path))
        {
            return "/";
        }

        if (path[0] != '/')
        {
            return "/" + path;
        }

        return path;
    }


    /// <summary>
    ///     Creates a new URI from a file system path, e.g. `c:\my\files`,
    ///     `/usr/home`, or `\\server\share\some\path`.
    ///     The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument
    ///     as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as**
    ///     `URI.parse('file://' + path)` because the path might contain characters that are
    ///     interpreted (# and ?). See the following sample:
    ///     <code>
    /// const good = URI.file('/coding/c#/project1');
    /// good.scheme === 'file';
    /// good.path === '/coding/c#/project1';
    /// good.fragment === '';
    /// const bad = URI.parse('file://' + '/coding/c#/project1');
    /// bad.scheme === 'file';
    /// bad.path === '/coding/c'; // path is now broken
    /// bad.fragment === '/project1';
    /// </code>
    /// </summary>
    /// <param name="path"></param>
    /// <returns></returns>
    /// <exception cref="UriFormatException"></exception>
    public static DocumentUri File(string path)
    {
        if (string.IsNullOrWhiteSpace(path))
        {
            throw new UriFormatException("Given path is null or empty");
        }

        string authority = string.Empty;

        // normalize to fwd-slashes on windows,
        // on other systems bwd-slashes are valid
        // filename character, eg /f\oo/ba\r.txt
        if (path[0] != '/')
        {
            path = path.Replace('\\', '/');
        }

        // check for authority as used in UNC shares
        // or use the path as given
        if (path.Length > 1 && path[0] == '/' && path[1] == '/')
        {
            int index = path.IndexOf('/', 2);
            if (index != -1)
            {
                authority = path[2..index];
                path = path[index..];
            }
            else
            {
                authority = path[2..];
                path = "/";
            }
        }

        path = path.Replace("%3A", ":", StringComparison.OrdinalIgnoreCase);

        return new DocumentUri("file", authority, path, string.Empty, string.Empty);
    }


    public DocumentUri With(DocumentUriComponents components)
    {
        return new DocumentUri(
            components.Scheme ?? Scheme,
            components.Authority ?? Authority,
            components.Path ?? Path,
            components.Query ?? Query,
            components.Fragment ?? Fragment);
    }


    public static DocumentUri From(DocumentUriComponents components)
    {
        return new DocumentUri(
            components.Scheme ?? string.Empty,
            components.Authority ?? string.Empty,
            components.Path ?? string.Empty,
            components.Query ?? string.Empty,
            components.Fragment ?? string.Empty);
    }


    public static DocumentUri From(string url)
    {
        if (string.IsNullOrWhiteSpace(url))
        {
            throw new UriFormatException("Given path is null or empty");
        }

        if (url.StartsWith("\\\\") || url.StartsWith('/') || GetWindowsPathRegex().IsMatch(url))
        {
            return DocumentUriCache.GetCached(url);
        }

        return Parse(url);
    }


    /// <summary>
    ///     Creates a new URI from a string, e.g. http://www.msft.com/some/path,
    ///     file:///usr/home, or scheme:with/path.
    /// </summary>
    /// <param name="value"></param>
    /// <param name="strict"></param>
    /// <returns></returns>
    public static DocumentUri Parse(string value, bool strict = false)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new UriFormatException("Given path is null or empty");
        }

        Match match = GetUriRegex().Match(value);

        if (!match.Success)
        {
            return Empty;
        }

        return new DocumentUri(
            match.Groups[2].Value,
            Unescape(match.Groups[4].Value),
            Unescape(match.Groups[5].Value),
            Unescape(match.Groups[7].Value),
            Unescape(match.Groups[9].Value),
            strict
        );
    }


    [GeneratedRegex(@"^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?")]
    private static partial Regex GetUriRegex();


    [GeneratedRegex(@"(%[0-9A-Za-z][0-9A-Za-z])+")]
    private static partial Regex GetEscapeDataStringRegex();


    private static string UnescapeDataString(string value)
    {
        try
        {
            return Uri.UnescapeDataString(value);
        }
        catch
        {
            if (value.Length > 3)
            {
                return value[..3] + UnescapeDataString(value[3..]);
            }

            return value;
        }
    }


    private static string Unescape(string value)
    {
        Regex regex = GetEscapeDataStringRegex();

        if (!regex.IsMatch(value))
        {
            return value;
        }

        return regex.Replace(value, match => UnescapeDataString(match.Value));
    }


    public override string ToString()
    {
        StringBuilder res = new();

        if (!string.IsNullOrWhiteSpace(Scheme))
        {
            res.Append(Scheme).Append(':');
        }

        string authority = Authority;

        if (!string.IsNullOrWhiteSpace(authority))
        {
            if (Scheme == "file")
            {
                res.Append("//");
            }

            int index = authority.IndexOf('@', StringComparison.Ordinal);
            if (index != -1)
            {
                // <user>@<authority>
                string user = authority[..index];
                authority = authority[(index + 1)..];
                index = user.IndexOf(':', StringComparison.Ordinal);
                if (index == -1)
                {
                    res.Append(EncodeUriComponent(user));
                }
                else
                {
                    // <user>:<pass>@<authority>
                    res.Append(EncodeUriComponent(user[..index]));
                    res.Append(':');
                    res.Append(EncodeUriComponent(user[(index + 1)..]));
                }

                res.Append('@');
            }

            authority = authority.ToLowerInvariant();
            index = authority.IndexOf(':', StringComparison.Ordinal);
            if (index == -1)
            {
                res.Append(EncodeUriComponent(authority));
            }
            else
            {
                // <authority>:<port>
                res.Append(EncodeUriComponent(authority[..index]));
                res.Append(authority[index..]);
            }
        }

        if (!string.IsNullOrWhiteSpace(Path))
        {
            bool appended = false;
            // lower-case windows drive letters in /C:/fff or C:/fff
            if (Path is ['/', _, ':', ..])
            {
                char code = Path[1];
                if (code is >= 'A' and <= 'Z')
                {
                    appended = true;
                    res.Append('/')
                        .Append(Convert.ToChar(code + 32))
                        .Append(':')
                        .Append(EncodeUriComponent(Path[3..], true));
                }
            }
            else if (Path is [_, ':', ..])
            {
                char code = Path[0];
                if (code is >= 'A' and <= 'Z')
                {
                    appended = true;
                    res.Append(Convert.ToChar(code + 32))
                        .Append(':')
                        .Append(EncodeUriComponent(Path[2..], true));
                }
            }

            if (!appended)
            {
                res.Append(EncodeUriComponent(Path, true));
            }
        }

        if (!string.IsNullOrWhiteSpace(Query))
        {
            res.Append('?').Append(EncodeUriComponent(Query));
        }

        if (!string.IsNullOrWhiteSpace(Fragment))
        {
            res.Append('#').Append(EncodeUriComponent(Fragment));
        }

        return res.ToString();
    }


    private static string EncodeUriComponent(string uriComponent, bool allowSlash = false)
    {
        StringBuilder? res = null;
        int pos = -1;

        for (int p = 0; p < uriComponent.Length; p++)
        {
            char ch = uriComponent[p];

            // unreserved characters: https://tools.ietf.org/html/rfc3986#section-2.3
            if (ch is >= 'a' and <= 'z' ||
                ch is >= 'A' and <= 'Z' ||
                ch is >= '0' and <= '9' ||
                ch == '.' ||
                ch == '-' ||
                ch == '_' ||
                ch == '~' ||
                (allowSlash && ch == '/') ||
                (allowSlash && p is 1 or 2 && uriComponent is ['/', _, ':', ..] or [_, ':', ..]))
            {
                if (pos != -1)
                {
                    res ??= new StringBuilder();
                    res.Append(Uri.EscapeDataString(uriComponent[pos..p]));
                    pos = -1;
                }

                res?.Append(ch);
            }
            else
            {
                if (res == null)
                {
                    res = new StringBuilder();
                    res.Append(uriComponent[..p]);
                }

                if (EscapeMap.TryGetValue(ch, out string? escaped))
                {
                    if (pos != -1)
                    {
                        res.Append(Uri.EscapeDataString(uriComponent[pos..p]));
                        pos = -1;
                    }

                    res.Append(escaped);
                }
                else if (pos == -1)
                {
                    pos = p;
                }
            }
        }

        if (pos != -1)
        {
            res ??= new StringBuilder();
            res.Append(Uri.EscapeDataString(uriComponent[pos..]));
        }

        return res != null ? res.ToString() : uriComponent;
    }


    [GeneratedRegex(@"^\w(?:\:|%3a)[\\|\/]", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
    private static partial Regex GetWindowsPathRegex();


    /// <summary>
    ///     Compute `fsPath` for the given uri
    /// </summary>
    public static string UriToFsPath(DocumentUri uri, bool keepDriveLetterCasing)
    {
        string value;

        if (!string.IsNullOrWhiteSpace(uri.Authority) &&
            uri.Path.Length > 1 &&
            uri.Scheme == "file")
        {
            // file://shares/c$/far/boo
            value = $@"\\{uri.Authority}{uri.Path}";
        }
        // windows path
        else if (uri.Path is ['/', _, _, ..] &&
                 ((uri.Path[1] >= 'a' && uri.Path[1] <= 'z') || (uri.Path[1] >= 'A' && uri.Path[1] <= 'Z')) &&
                 uri.Path[2] == ':')
        {
            if (!keepDriveLetterCasing)
            {
                // windows : file:///c:/far/boo
                value = char.ToLowerInvariant(uri.Path[1]) + uri.Path[2..];
            }
            else
            {
                value = uri.Path[1..];
            }
        }
        // other
        else
        {
            value = uri.Path;
        }

        if (value.StartsWith("\\\\") || GetWindowsPathRegex().IsMatch(value))
        {
            return value.Replace('/', '\\');
        }

        return value;
    }

    public override bool Equals(object? obj)
    {
        return Equals(obj as DocumentUri);
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Scheme, Fragment, Authority, Query, Path);
    }
}


public class DocumentUriComparer : IEqualityComparer<DocumentUri>
{
    private DocumentUriComparer()
    {
    }


    public static DocumentUriComparer Default { get; } = new();


    public bool Equals(DocumentUri? x, DocumentUri? y)
    {
        if (x == null)
        {
            return y == null;
        }

        return x.Equals(y);
    }

    public int GetHashCode(DocumentUri obj)
    {
        return obj.GetHashCode();
    }
}


public static class DocumentUriCache
{
    private static readonly Dictionary<string, DocumentUri> Cache = new(StringComparer.OrdinalIgnoreCase);


    public static DocumentUri GetCached(string filePath)
    {
        if (Cache.TryGetValue(filePath, out DocumentUri? uri))
        {
            return uri;
        }

        uri = DocumentUri.File(filePath);
        Cache.Add(filePath, uri);
        return uri;
    }
}